#region License
// Copyright (c) 2007 James Newton-King
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
#endregion

using System;
using System.Collections.Generic;
using System.Reflection.Emit;
using System.Reflection;
using Newtonsoft.Utilities.Miscellaneous;

namespace Newtonsoft.Utilities.Reflection
{
  public enum Operation
  {
    Add,
    Subtract,
    Multiply,
    Divide,
    And,
    Or,
    Xor,
    Not
  }

  public struct GenericOperationKey : IEquatable<GenericOperationKey>
  {
    private readonly Operation _operation;
    private readonly Type _left;
    private readonly Type _right;
    private readonly Type _result;

    public GenericOperationKey(Operation operation, Type left, Type right, Type result)
    {
      _operation = operation;
      _left = left;
      _right = right;
      _result = result;
    }

    public override bool Equals(object obj)
    {
      if (obj is GenericOperationKey)
        return Equals((GenericOperationKey)obj);
      else
        return false;
    }

    public bool Equals(GenericOperationKey other)
    {
      return (_operation == other._operation
              && _left == other._left
              && _right == other._right
              && _result == other._result);
    }

    public override int GetHashCode()
    {
      return _operation.GetHashCode()
        ^ MiscellaneousUtils.GetHashCode(_left)
        ^ MiscellaneousUtils.GetHashCode(_right)
        ^ MiscellaneousUtils.GetHashCode(_result);
    }
  }

  public static class OperatorFactory
  {
    private static Dictionary<GenericOperationKey, Delegate> _cachedDelegates = new Dictionary<GenericOperationKey, Delegate>();

    private static Func<TLeft, TRight, TResult> CreateBinaryExpression<TLeft, TRight, TResult>(Operation operation)
    {
      DynamicMethod method = new DynamicMethod(
        operation + ":" + typeof(TLeft) + ":" + typeof(TRight) + ":" + typeof(TResult) + ":" + typeof(OperatorFactory),
        typeof(TResult),
        new Type[] { typeof(TLeft), typeof(TRight) },
        typeof(OperatorFactory)
      );

      ILGenerator generator = method.GetILGenerator();

      generator.Emit(OpCodes.Ldarg_0);
      generator.Emit(OpCodes.Ldarg_1);

      if (typeof(TLeft).IsPrimitive || typeof(TLeft).IsEnum)
      {
        generator.Emit(GetOperationOpCode(operation));
      }
      else
      {
        MethodInfo info = typeof(TLeft).GetMethod(
          GetOperationMethodName(operation),
          new Type[] { typeof(TLeft), typeof(TRight) },
          null
          );

        generator.EmitCall(OpCodes.Call, info, null);
      }

      generator.Emit(OpCodes.Ret);

      return (Func<TLeft, TRight, TResult>)method.CreateDelegate(typeof(Func<TLeft, TRight, TResult>));
    }

    private static Func<TLeft, TRight, TResult> GetCachedDefault<TLeft, TRight, TResult>(Operation operation)
    {
      return GetCached(operation, new Func<Operation, Func<TLeft, TRight, TResult>>(CreateBinaryExpression<TLeft, TRight, TResult>));
    }

    private static Func<TLeft, TRight, TResult> GetCached<TLeft, TRight, TResult>(Operation operation, Func<Operation, Func<TLeft, TRight, TResult>> createDelegate)
    {
      ValidationUtils.ArgumentNotNull(createDelegate, "createDelegate");

      GenericOperationKey key = new GenericOperationKey(operation, typeof(TLeft), typeof(TRight), typeof(TResult));

      Delegate operatorDelegate;
      if (!_cachedDelegates.TryGetValue(key, out operatorDelegate))
      {
        operatorDelegate = createDelegate(operation);
        _cachedDelegates[key] = operatorDelegate;
      }

      return (Func<TLeft, TRight, TResult>)operatorDelegate;
    }

    private static string GetOperationMethodName(Operation operation)
    {
      switch (operation)
      {
        case Operation.Add:
          return "op_Addition";
        case Operation.Subtract:
          return "op_Subtraction";
        case Operation.Multiply:
          return "op_Multiply";
        case Operation.Divide:
          return "op_Devision";
        default:
          throw new Exception(string.Format("No method for operation {0}.", operation));
      }
    }

    private static OpCode GetOperationOpCode(Operation operation)
    {
      switch (operation)
      {
        case Operation.Add:
          return OpCodes.Add;
        case Operation.Subtract:
          return OpCodes.Sub;
        case Operation.Multiply:
          return OpCodes.Mul;
        case Operation.Divide:
          return OpCodes.Div;
        case Operation.And:
          return OpCodes.And;
        case Operation.Or:
          return OpCodes.Or;
        case Operation.Xor:
          return OpCodes.Xor;
        case Operation.Not:
          return OpCodes.Not;
        default:
          throw new Exception(string.Format("No OpCode for operation {0}.", operation));
      }
    }

    public static Func<T, T, T> GetAdd<T>()
    {
      return GetAdd<T, T, T>();
    }

    public static Func<TLeft, TRight, TResult> GetAdd<TLeft, TRight, TResult>()
    {
      return GetCachedDefault<TLeft, TRight, TResult>(Operation.Add);
    }

    public static Func<T, T, T> GetSubtract<T>()
    {
      return GetSubtract<T, T, T>();
    }

    public static Func<TLeft, TRight, TResult> GetSubtract<TLeft, TRight, TResult>()
    {
      return GetCachedDefault<TLeft, TRight, TResult>(Operation.Subtract);
    }

    public static Func<T, T, T> GetMultiply<T>()
    {
      return GetMultiply<T, T, T>();
    }

    public static Func<TLeft, TRight, TResult> GetMultiply<TLeft, TRight, TResult>()
    {
      return GetCachedDefault<TLeft, TRight, TResult>(Operation.Multiply);
    }

    public static Func<T, T, T> GetDivide<T>()
    {
      return GetDivide<T, T, T>();
    }

    public static Func<TLeft, TRight, TResult> GetDivide<TLeft, TRight, TResult>()
    {
      return GetCachedDefault<TLeft, TRight, TResult>(Operation.Divide);
    }

    public static Func<T, T, T> GetAnd<T>()
    {
      return GetAnd<T, T, T>();
    }

    public static Func<TLeft, TRight, TResult> GetAnd<TLeft, TRight, TResult>()
    {
      return GetCachedDefault<TLeft, TRight, TResult>(Operation.And);
    }

    public static Func<T, T, T> GetOr<T>()
    {
      return GetOr<T, T, T>();
    }

    public static Func<TLeft, TRight, TResult> GetOr<TLeft, TRight, TResult>()
    {
      return GetCachedDefault<TLeft, TRight, TResult>(Operation.Or);
    }

    public static Func<T, T, T> GetXor<T>()
    {
      return GetXor<T, T, T>();
    }

    public static Func<TLeft, TRight, TResult> GetXor<TLeft, TRight, TResult>()
    {
      return GetCachedDefault<TLeft, TRight, TResult>(Operation.Xor);
    }

    public static Func<T, T, T> GetNot<T>()
    {
      return GetNot<T, T, T>();
    }

    public static Func<TLeft, TRight, TResult> GetNot<TLeft, TRight, TResult>()
    {
      return GetCachedDefault<TLeft, TRight, TResult>(Operation.Not);
    }
  }
}